/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.mapping;

import java.util.*;
import javax.swing.*;
import javax.swing.table.*;
import org.jdesktop.beansbinding.*;
import org.jdesktop.beansbinding.AutoBinding.UpdateStrategy;
import org.jdesktop.swingbinding.*;
import ch.trackedbean.binding.components.*;
import ch.trackedbean.binding.components.renderers.*;
import ch.trackedbean.binding.internal.mapping.*;
import ch.trackedbean.loaders.*;
import ch.trackedbean.tracking.*;
import ch.trackedbean.tracking.internal.*;

/**
 * Mapping for {@link JTable}s.<br>
 * Consider using the {@link TablePanel}.
 * 
 * @author M. Hautle
 * @param <T> The bean type
 */
public class TableMapping<T> {
    /** The name of the model property. */
    public static final String MODEL_PROPERTY = "model";

    // CHECKME property validation?

    /** Property for table selection */
    private static final Property TABLE_SELECTION_PROP = BeanProperty.<JTable, Object> create("selectedElement_IGNORE_ADJUSTING");

    /** Property for {@link MappingGroup} */
    private static final Property MAPPING_GRP_PROP = BeanProperty.<MappingGroup, Object> create("model");

    /** The mapped table */
    private final JTable table;

    /** The binding of the table */
    private final JTableBinding<T, List<T>, JTable> binding;

    /** The selection mapping or null */
    private Map<String, Binding> selectionBindings = null;

    /** Map containing informations for the column bindings */
    private final Map<JTableBinding<T, List<T>, JTable>.ColumnBinding, ColumnBindingInfos> bindingInfos = new IdentityHashMap<JTableBinding<T, List<T>, JTable>.ColumnBinding, ColumnBindingInfos>();

    /** The list used as table model */
    private List<T> model;

    /** Flag indicating if the table is read only. */
    private boolean readOnly;

    /**
     * Default constructor
     * 
     * @param table The table to map
     * @param readOnly True if the table is readonly
     */
    public TableMapping(final JTable table, boolean readOnly) {
        this.table = table;
        this.readOnly = readOnly;
        binding = SwingBindings.createJTableBinding(AutoBinding.UpdateStrategy.READ, model, table);
    }

    /**
     * Adds a selection binding to the given {@link MappingGroup}.<br>
     * If a binding with the same name exists already, then it's removed before the new one is added.
     * 
     * @param name The name of the selection binding
     * @param group The binding group to add
     */
    @SuppressWarnings("unchecked")
    public void addSelectionBinding(final String name, final MappingGroup<T> group) {
        final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, TABLE_SELECTION_PROP, group, MAPPING_GRP_PROP);
        addSelectionBinding(name, b);
    }

    /**
     * Adds a selection binding to the given {@link MappingGroup}.<br>
     * If a binding with the same name exists already, then it's removed before the new one is added.
     * 
     * @param name The name of the selection binding
     * @param relativePath The path starting at the returned row
     * @param group The binding group to add
     */
    @SuppressWarnings("unchecked")
    public void addSelectionBinding(final String name, final String relativePath, final MappingGroup group) {
        final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, BeanProperty.create(TABLE_SELECTION_PROP, relativePath), group,
                MAPPING_GRP_PROP);
        addSelectionBinding(name, b);
    }

    /**
     * Adds a selection binding to the given bean property.<br>
     * If a binding with the same name exists already, then it's removed before the new one is added.
     * 
     * @param name The name of the selection binding
     * @param dst The destination bean
     * @param dstPath The path to the destination property
     */
    @SuppressWarnings("unchecked")
    public void addSelectionBinding(final String name, final Object dst, final String dstPath) {
        final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, TABLE_SELECTION_PROP, dst, BeanProperty.create(dstPath));
        addSelectionBinding(name, b);
    }

    /**
     * Adds a selection binding to the given bean property.<br>
     * If a binding with the same name exists already, then it's removed before the new one is added.
     * 
     * @param name The name of the selection binding
     * @param relativePath The path starting at the returned row
     * @param dst The destination bean
     * @param dstPath The path to the destination property
     */
    @SuppressWarnings("unchecked")
    public void addSelectionBinding(final String name, final String relativePath, final Object dst, final String dstPath) {
        final Binding b = Bindings.createAutoBinding(AutoBinding.UpdateStrategy.READ, table, BeanProperty.create(TABLE_SELECTION_PROP, relativePath), dst,
                BeanProperty.create(dstPath));
        addSelectionBinding(name, b);
    }

    /**
     * Adds the given binding as selection binding.<br>
     * If a binding with the same name exists already, then it's removed before the new one is added.
     * 
     * @param name The name of the selection binding
     * @param b The binding
     */
    private void addSelectionBinding(final String name, final Binding b) {
        if (selectionBindings == null)
            selectionBindings = new HashMap<String, Binding>();
        final Binding old = selectionBindings.put(name, b);
        if (old != null)
            old.unbind();
        b.bind();
    }

    /**
     * Removes the selection binding with the given name
     * 
     * @param name The name of the selection binding
     */
    public void removeSelectionBinding(final String name) {
        if (selectionBindings == null)
            return;
        final Binding b = selectionBindings.remove(name);
        if (b != null)
            b.unbind();
    }

    /**
     * Adds a column binding
     * 
     * @param label The column label
     * @param path The path to the property shown in this column
     * @param editable True if the column is editable
     * @return The {@link BindingInfos} object for further configuration of the binding
     * @throws IllegalStateException if the table is already bound
     */
    public ColumnBindingInfos addColumn(final String label, final String path, final boolean editable) {
        return addColumn(label, path, null, editable);
    }

    /**
     * Adds a column binding
     * 
     * @param label The column label
     * @param path The path to the property shown in this column (simple or nested property)
     * @param type The type of this column (may be null)
     * @param editable True if the column is editable
     * @return The {@link BindingInfos} object for further configuration of the binding
     * @throws IllegalStateException if the table is already bound
     */
    public ColumnBindingInfos addColumn(final String label, final String path, final Class<?> type, final boolean editable) {
        return addColumn(label, path, type, BeanProperty.<T, Object> create(path), editable);
    }

    /**
     * Adds a column binding
     * 
     * @param label The column label
     * @param path The path to the property shown in this column
     * @param type The type of this column (may be null)
     * @param accessor The accessor to use for the binding itself
     * @param editable True if the column is editable
     * @return The {@link BindingInfos} object for further configuration of the binding
     * @throws IllegalStateException if the table is already bound
     */
    public ColumnBindingInfos addColumn(final String label, final String path, final Class<?> type, final Property<T, Object> accessor, final boolean editable) {
        if (model != null)
            throw new IllegalStateException("Can't add a column if the table is already bound");
        final JTableBinding<T, List<T>, JTable>.ColumnBinding b = binding.addColumnBinding(accessor);
        b.setColumnName(label);
        b.setColumnClass(type);
        final ColumnBindingInfos i = new ColumnBindingInfos(path);
        i.setEditable(editable);
        bindingInfos.put(b, i);
        return i;
    }

    /**
     * Adds a column binding with a {@link JComboBox} using a {@link SimpleELListRenderer} with the given el expression.<br>
     * The combobox content list will be loaded using {@link DataManager#getList(Class, String, Map)}.
     * 
     * @param label The column label
     * @param path The path to the property shown in this column
     * @param el The simple el expression to use by the combobox renderer
     * @param type The content type of the combobox list
     * @param subType Additional discriminator specifying the "subtype" of the combobox content list (may be null)
     * @param settings Additional settings for the combobox content load (may be null)
     * @param showStatus True if the cell background should be adapted according to the cell state
     * @return The {@link BindingInfos} object for further configuration of the binding
     * @throws IllegalStateException if the table is already bound
     */
    public ColumnBindingInfos addComboBoxColumn(final String label, final String path, String el, Class<?> type, Object subType, Map settings,
            boolean showStatus) {
        final ColumnBindingInfos i = addColumn(label, path, true);
        final JComboBox combobox = new JComboBox();
        combobox.setRenderer(new SimpleELListRenderer(el, ""));
        final JComboBoxBinding<Object, List<Object>, JComboBox> listBinding = SwingBindings.createJComboBoxBinding(UpdateStrategy.READ, null, combobox);
        i.setRenderer(new SimpleELTableCellRenderer(el, "", this, showStatus));
        i.setEditor(new DefaultCellEditor(combobox));
        i.setSubBinding(new ListBindingInfos(listBinding, type, subType, settings));
        return i;
    }

    /**
     * Adds a column binding to a status property
     * 
     * @param label The column label
     * @param path The path to the property
     * @throws IllegalStateException if the table is already bound
     */
    public void addStatusColumn(final String label, final String path) {
        if (model != null)
            throw new IllegalStateException("Can't add a column if the table is already bound");
        final JTableBinding<T, List<T>, JTable>.ColumnBinding b = binding.addColumnBinding(StatusProperty.<T> create(path));
        b.setColumnName(label);
        b.setColumnClass(Status.class);
        b.setEditable(false);
    }

    /**
     * Removes the binding for the column with the given index
     * 
     * @param col The column to remove
     * @return True if the column was removed
     * @throws IllegalStateException if the table is already bound
     */
    public boolean removeColumnBinding(final int col) {
        if (model != null)
            throw new IllegalStateException("Can't remove a column if the table is already bound");
        if (col >= table.getColumnCount())
            return false;
        final JTableBinding<T, List<T>, JTable>.ColumnBinding bn = binding.removeColumnBinding(col);
        bindingInfos.remove(bn);
        return true;
    }

    /**
     * Returns the status of the value in the given cell
     * 
     * @param row The row
     * @param col The column
     * @return The status of the value in the given cell or null if the cell doesen't exists
     */
    public Status getCellStatus(final int row, final int col) {
        if (row >= model.size() || col > table.getColumnCount())
            return null;
        final BindingInfos info = bindingInfos.get(binding.getColumnBinding(table.convertColumnIndexToModel(col)));
        if (info == null)
            return null;
        return TrackedBeanUtils.getPropertyStatus(model.get(row), info.getPath());
    }

    /**
     * Returns if the table is readonly.
     * 
     * @return If the table is readOnly
     */
    public boolean isReadOnly() {
        return readOnly;
    }

    /**
     * Sets if the table is read only
     * 
     * @param readOnly True if the table has to be readonly
     */
    public void setReadOnly(final boolean readOnly) {
        if (model != null)
            throw new IllegalStateException("Can't set the table to readonly if it's already bound");
        this.readOnly = readOnly;
    }

    /**
     * Sets the given model.<br>
     * If null is passed the current model will be removed.<br>
     * If the table is already bound to a model then the table will be rebound to the new model.
     * 
     * @param model The model to set or null to unset the current model
     */
    public void setModel(final List<T> model) {
        if (this.model != null)
            removeModel();
        if (model != null)
            bind(model);
    }

    /**
     * Returns the mapped table.
     * 
     * @return The table
     */
    public JTable getTable() {
        return table;
    }

    /**
     * Returns the list model
     * 
     * @return The list model or null
     */
    public List<T> getModel() {
        return model;
    }

    /**
     * Returns the object representing the selected row.
     * 
     * @return The selected object or null
     */
    public T getSelectedRow() {
        final int row = table.getSelectedRow();
        if (row == -1)
            return null;
        return model.get(table.convertRowIndexToModel(row));
    }

    /**
     * Returns a list of all selected rows.
     * 
     * @return The selected rows
     */
    public List<T> getSelectedRows() {
        final int cnt = table.getSelectedRowCount();
        if (cnt == 0)
            return Collections.emptyList();
        // fill the selection list
        final List<T> res = new ArrayList<T>(cnt);
        for (int i : table.getSelectedRows())
            res.add(model.get(table.convertRowIndexToModel(i)));
        return res;
    }

    /**
     * Binds the table to the given model
     * 
     * @param list The model
     */
    private void bind(final List<T> list) {
        this.model = list;
        // configure the bindings
        for (final JTableBinding<T, List<T>, JTable>.ColumnBinding b : binding.getColumnBindings()) {
            final ColumnBindingInfos info = bindingInfos.get(b);
            if (info == null)
                continue;
            info.configure(b);
            info.bindSubBinding();
        }
        binding.setSourceObject(list);
        binding.setEditable(!readOnly);
        binding.bind();
        configureTable();
    }

    /**
     * Configures the table.<br>
     * This method has to be called after the model was bound!
     */
    private void configureTable() {
        final TableColumnModel cols = table.getColumnModel();
        for (final JTableBinding<T, List<T>, JTable>.ColumnBinding b : binding.getColumnBindings()) {
            final ColumnBindingInfos info = bindingInfos.get(b);
            if (info != null)
                configureColumn(cols.getColumn(b.getColumn()), info);
        }
    }

    /**
     * Configures the given column according to the binding info.
     * 
     * @param col The column
     * @param infos The binding info
     */
    private void configureColumn(TableColumn col, ColumnBindingInfos infos) {
        col.setCellRenderer(infos.getRenderer());
        col.setCellEditor(infos.getEditor());
    }

    /**
     * Removes the current model
     */
    private void removeModel() {
        if (binding.isBound())
            binding.unbind();
        this.model = null;
    }

    /**
     * Returns the binding information for all column bindings.
     * 
     * @return The bindings
     */
    public List<ColumnBindingInfos> getBindings() {
        return new ArrayList<ColumnBindingInfos>(bindingInfos.values());
    }
}
